<?php
// +----------------------------------------------------------------------
// | INPHP
// | Copyright (c) https://inphp.cc All rights reserved.
// | 该文件源码由INPHP官方提供，使用协议以INPHP官方公告为准。
// +----------------------------------------------------------------------
// | 退款
// +----------------------------------------------------------------------
namespace app\finance\onlinePay\wechat;

use app\finance\model\CashierModel;
use app\finance\model\RefundModel;
use app\finance\onlinePay\IRefund;
use Inphp\Core\Object\Message;
use Inphp\Core\Services\Http\Response;
use Inphp\Core\Util\Log;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;

class refund implements IRefund
{
    public function process(array $arg = []): Message
    {
        // TODO: Implement process() method.
        //退款金额
        $amount = $arg["amount"] ?? 0;
        $amount = is_numeric($amount) && $amount > 0 ? $amount : 0;
        if ($amount <= 0) {
            return httpMessage("退款金额必须大于0");
        }
        //原订单付款总金额
        $payedAmount = $arg["payedAmount"] ?? 0;
        $payedAmount = is_numeric($payedAmount) && $payedAmount > 0 ? $payedAmount : 0;
        if ($payedAmount <= 0) {
            return httpMessage("原订单金额参数无效");
        }
        //退款单ID
        $refundId = $arg["refundId"] ?? 0;
        if (empty($refundId)) {
            return httpMessage("缺少退款单号");
        }
        //支付交易使用的订单号
        $payedTradeNo = $arg["payedTradeNo"] ?? null;
        if (empty($payedTradeNo)) {
            return httpMessage("缺少支付平台订单号");
        }
        //退款时，可能传入支付时使用的APP ID
        $appId = $arg["appId"] ?? null;
        //取配置
        $payments = CashierModel::onlinePayments();
        $config = $payments["wechat"] ?? null;
        if (empty($config)) {
            return httpMessage("缺少配置参数");
        }
        if (!$config["enable"]) {
            return httpMessage("该支付方式暂未开放");
        }
        $appList = $config["appList"] ?? [];
        if (empty($appList)) {
            return httpMessage("没有可用的支付应用");
        }
        $app = !empty($appId) ? ($appList[$appId] ?? reset($appList)) : reset($appList);
        $app = is_array($app) ? $app : [];
        if (empty($app)) {
            return httpMessage("未配置的支付应用");
        }
        $appId = $app["appId"] ?? $appId;
        if (empty($appId)) {
            return httpMessage("缺少支付应用ID");
        }
        //开发模式
        $dev = isset($app["dev"]) && $app["dev"] === true;
        //如果是开发模式，将把支付金额改为0.01元
        $amount = $dev ? 0.01 : $amount;
        //域名地址
        $host = getHost();
        //异步通知地址
        $notifyUrl = $host."/finance/pay/refund/wechat_{$appId}";
        //$notifyUrl = $app["notifyUrl"] ?? $notifyUrl;
        //订单号前缀
        $outTradeNoPrefix = $app["outTradeNoPrefix"] ?? "";
        //生成单号
        $outRefundNo = $outTradeNoPrefix.$refundId;
        //构造数据
        //证书私钥
        $privateKeyInstance = \WeChatPay\Crypto\Rsa::from($app["key"], \WeChatPay\Crypto\Rsa::KEY_TYPE_PRIVATE);
        //公钥
        $publicKeyInstance = \WeChatPay\Crypto\Rsa::from($app["cert"], \WeChatPay\Crypto\Rsa::KEY_TYPE_PUBLIC);
        //取得证书序列号
        $platformCertSN = \WeChatPay\Util\PemUtil::parseCertificateSerialNo($app["cert"]);
        //构造APIv3客户端
        $instance = \WeChatPay\Builder::factory([
            "mchid"     => (string) $app["mchId"],
            "serial"    => (string) $app["certSN"],
            "privateKey"=> $privateKeyInstance,
            "certs"     => [
                $platformCertSN => $publicKeyInstance
            ]
        ]);
        if (!$instance) {
            return httpMessage("未能构建v3支付客户对象");
        }
        //退款
        $chain = "v3/refund/domestic/refunds";
        //构造数据
        $body = [
            //微信交易订单号
            "transaction_id"        => (string) $payedTradeNo,
            //本次要退款的内部退款单号
            "out_refund_no"         => (string) $outRefundNo,
            //退款原因
            "reason"                => $arg["reason"] ?? "标准退款",
            //异步通知地址
            "notify_url"            => $notifyUrl,
            //退款金额
            "amount"                => [
                //退款金额
                "refund"    => floor($amount * 100),
                //原订单金额
                "total"     => floor($payedAmount * 100),
                //币种
                "currency"  => "CNY"
            ]
        ];
        //
        try{
            //$privateKeyInstance = Rsa::from($config["key"], Rsa::KEY_TYPE_PRIVATE);
            $response = $instance->chain($chain)
                ->post(["json" => $body]);
            if ($response->getStatusCode() === 200) {
                $content = $response->getBody()->getContents();
                $json = !empty($content) ? (@json_decode($content, true) ?? null) : null;
                if (is_array($json) && isset($json["refund_id"])) {
                    return httpMessage(0, $json["status"], [
                        //实际退款金额
                        "refundAmount"      => bcdiv($json["amount"]["payer_refund"], 100, 2),
                        //退款 ID
                        "refundId"          => $json["refund_id"],
                        //接口响应数据，可用于保存备份
                        "result"            => $json
                    ]);
                }
                return httpMessage("未取得退款会话标识".(isset($json["code"]) ? ("，错误码：{$json["code"]}/{$json["message"]}") : ""));
            }
            return httpMessage("调用微信退款接口失败，未能正确返回数据，HTTP状态码：{$response->getStatusCode()}");
        } catch (\Exception $exception) {
            //错误记录到文件
            Log::writeToEnd(RUNTIME."/logs/wechatPayRefund.txt", $exception);
            return httpMessage(1, $exception->getMessage());
        }
    }

    /**
     * 使用异步通知处理
     * @param string|null $appId
     */
    public function notify(?string $appId = null)
    {
        // TODO: Implement notify() method.
        $client = getClient();
        $this->saveLog(json_encode(["headers" => $client->header, "post" => $client->post, "get" => $client->get, "raw" => $client->rawData], 256));
        //从头部获取参数
        $inWechatPaySignature = HEADERS("Wechatpay-Signature");
        $inWechatPayTimestamp = HEADERS("Wechatpay-Timestamp");
        $inWechatPayTimestamp = is_numeric($inWechatPayTimestamp) ? $inWechatPayTimestamp : 0;
        $inWechatPayNonce = HEADERS("Wechatpay-Nonce");
        $inWechatPaySerial = HEADERS("Wechatpay-Serial");
        $inWechatPayRequestId = HEADERS("Request-ID");
        //JSON已自动转化为POST参数
        $inBody = $client->rawData;
        //取配置
        $payments = CashierModel::onlinePayments();
        $config = $payments["wechat"] ?? null;
        if (empty($config)) {
            $this->saveLog("附加数据无效");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "缺少配置参数"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        $appList = $config["appList"] ?? [];
        if (empty($appList)) {
            $this->saveLog("没有可用的支付应用");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "没有可用的支付应用"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        $app = !empty($appId) ? ($appList[$appId] ?? reset($appList)) : reset($appList);
        $app = is_array($app) ? $app : [];
        if (empty($app)) {
            $this->saveLog("未配置的支付应用");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "未配置的支付应用"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        $appId = $app["appId"] ?? $appId;
        if (empty($appId)) {
            $this->saveLog("缺少支付应用ID");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "缺少支付应用ID"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        //
        $publicKeyInstance = Rsa::from($app["cert"], Rsa::KEY_TYPE_PUBLIC);
        // 检查通知时间偏移量，允许5分钟之内的偏移
        $timeOffsetStatus = abs(time() - (int)$inWechatPayTimestamp);
        if ($timeOffsetStatus >= 300) {
            $this->saveLog("时间偏移过大");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "时间偏移过大"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        $verifiedStatus = Rsa::verify(
            // 构造验签名串
            Formatter::joinedByLineFeed($inWechatPayTimestamp, $inWechatPayNonce, $inBody),
            $inWechatPaySignature,
            $publicKeyInstance
        );
        if (!$verifiedStatus) {
            $this->saveLog("构造验签名单失败");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "构造验签名单失败"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        // 转换通知的JSON文本消息为PHP Array数组
        $inBodyArray = (array) json_decode($inBody, true);
        // 使用PHP7的数据解构语法，从Array中解构并赋值变量
        ['resource' => [
            'ciphertext'      => $ciphertext,
            'nonce'           => $nonce,
            'associated_data' => $aad
        ]] = $inBodyArray;
        // 加密文本消息解密
        $inBodyResource = AesGcm::decrypt($ciphertext, $app["apiKey"], $nonce, $aad);
        // 把解密后的文本转换为PHP Array数组
        $inBodyResourceArray = (array)json_decode($inBodyResource, true);
        // print_r($inBodyResourceArray);// 打印解密后的结果
        $this->saveLog(json_encode($inBodyResourceArray, 256));
        //
        //关键数据需要记录的：trade_type(付款方式/交易类型：JSAP、APP...)
        //trade_state 交易状态
        if ($inBodyResourceArray["refund_status"] == "SUCCESS") {
            $cashierId = $inBodyResourceArray["out_trade_no"];
            $refundId = $inBodyResourceArray["out_refund_no"];
            //实际退款金额
            $refundAmount = bcdiv($inBodyResourceArray["amount"]["payer_refund"], 100, 2);
            if (!RefundModel::onlineRefundNotify([
                //收银单号
                "id"            => $cashierId,
                "cashierId"     => $cashierId,
                //退款时使用的内部退款单号
                "refundId"      => $refundId,
                //第三方退款 ID
                "outRefundId"   => $inBodyResourceArray["refund_id"],
                //本次退款金额
                "refundAmount"  => $refundAmount,
                //在线支付方式
                "payment"       => "wechat",
                //
                "result"        => $inBodyResourceArray
            ])) {
                $this->saveLog(json_encode(["cashierId" => $cashierId, "refundId" => $refundId, "message" => "未能处理退款通知"], 256));
            }
        }
        $this->saveLog("数据已接收");
        getHttpResponse()->withJson(["code" => "SUCCESS", "message" => "数据已接收"])->end();
    }

    private function saveLog(string $text) {
        $date = date("Ymd");
        Log::writeToEnd(RUNTIME."/logs/finance/refund/{$date}.txt", $text);
    }
}